package util;

import java.util.Collection;

/**
 * This is a class of {@link String} utility functions
 *
 * @author Neil Dickson
 */
public final class StringUtil {
	/**
	 * This splits a {@link String} into the lines it contains.  This is different from <code>aString.split("\\n")</code>
	 * in that this function properly handles end cases, i.e. there is an "" when '\n' is the first or last character.
	 * It also properly handles text with mixed CR, LF, and CRLF.
	 *
	 * @param text the text to split
	 * @return the lines of text in the {@link String}
	 */
	public static String[] getLines(String text) {
		String[] lines = new String[getLineCount(text)];
		int lineNum = 0;
		int start = 0;
		for (int i=0;i<text.length();++i) {
			if (text.charAt(i)=='\r') {
				lines[lineNum++] = text.substring(start,i);
				start = i+1;
				if (i+1<text.length() && text.charAt(i+1)=='\n') {
					++i;
					++start;
				}
			}
			else if (text.charAt(i)=='\n') {
				lines[lineNum++] = text.substring(start,i);
				start = i+1;
			}
		}
		lines[lineNum] = text.substring(start,text.length());
		return lines;
	}

	/**
	 * This counts the number of lines in the given {@link String}.
	 * This function properly handles end cases, i.e. there is an "" when '\n' is the first or last character.
	 * It also properly handles text with mixed CR, LF, and CRLF.
	 *
	 * @param text the text to scan
	 * @return the number of lines of text in the {@link String}
	 */
	public static int getLineCount(String text) {
		int count = 1;
		for (int i=0;i<text.length();++i) {
			if (text.charAt(i)=='\r') {
				++count;
				if (text.length()>i && text.charAt(i+1)=='\n') {
					++i;
				}
			}
			else if (text.charAt(i)=='\n') {
				++count;
			}
		}
		return count;
	}

	/**
	 * This isolates just the first of any lines of text.
	 * @param text the full text, whose first line is to be extracted
	 * @return the first line of text, with no line ending characters
	 */
	public static String getFirstLine(String text) {
		int LFIndex = text.indexOf('\n');
		int CRIndex = text.indexOf('\r');
		if (LFIndex>=0 && CRIndex>=0) {
			return text.substring(0,Math.min(LFIndex,CRIndex));
		}
		else if (LFIndex>=0) {
			return text.substring(0,LFIndex);
		}
		else if (CRIndex>=0) {
			return text.substring(0,CRIndex);
		}
		return text;
	}

	/**
	 * Takes a list of {@link String}s and formats the list as a single String with <code>", "</code> between each element
	 * @param list list of Strings from which to make the comma-space-separated list
	 * @return the comma-space-separated list representation of the specified list
	 */
	public static String toCommaSpaceSeparatedList(Collection<String> list) {
		StringBuffer buf = new StringBuffer();
		for (String string : list) {
			buf.append(string).append(", ");
		}
		if (buf.length()>=2) {
			buf.setLength(buf.length()-2);
		}
		else {
			buf.setLength(0);
		}
		return buf.toString();
	}

	/**
	 * Takes a list of {@link String}s and formats the list as a single String with <code>","</code> between each element
	 * @param list list of Strings from which to make the comma-separated list
	 * @return the comma-separated list representation of the specified list
	 */
	public static String toCommaSeparatedList(String[] list) {
		StringBuffer buf = new StringBuffer();
		for (String string : list) {
			buf.append(string).append(",");
		}
		if (buf.length()>=1) {
			buf.setLength(buf.length()-1);
		}
		return buf.toString();
	}

	/**
	 * Takes a string of comma-separated strings and creates an array of these strings
	 * @param s the string to split on commas
	 * @return an array of the list elements
	 */
	public static String[] fromCommaSeparatedList(String s) {
		int size = 1;
		for (int i=0;i<s.length();++i) {
			size += (s.charAt(i)==',')?1:0;
		}
		String[] list = new String[size];
		int index = 0;
		for (int i=0;i<size-1;++i) {
			int newIndex = s.indexOf(',',index);
			list[i] = s.substring(index,newIndex);
			index = newIndex+1;
		}
		list[list.length-1] = s.substring(index);
		return list;
	}

	/**
	 * @param s	String to scan for the first non-whitespace character
	 * @return the index of the first non-whitespace character, or the size of the String if it is entirely whitespace
	 */
	public static int getIndexAfterSpace(String s) {
		return getIndexAfterSpace(s,0);
	}

	/**
	 * @param s	String to scan for the first non-whitespace character
	 * @param index the index at which to start scanning
	 * @return the index of the first non-whitespace character, or the size of the String if it is entirely whitespace
	 */
	public static int getIndexAfterSpace(String s,int index) {
		// Consider anything that is a control character to be space
		while (index<s.length() && s.charAt(index)<=' ') {
			++index;
		}
		return index;
	}

	/**
	 * Finds the word starting at the specified location in the line
	 * @param line			the text in which to find a word
	 * @param start			the index at which to start looking for a word
	 * @param extra			characters other than letters and numbers to be counted as part of a word
	 * @return the word starting at the specified index, or the empty string if none
	 */
	public static String getWord(String line,int start,String extra) {
		return getWord(line,start,extra,"");
	}

	/**
	 * Finds the word starting at the specified location in the line
	 * @param line			the text in which to find a word
	 * @param start			the index at which to start looking for a word
	 * @param extra			characters other than letters and numbers to be counted as part of a word
	 * @param extraFirst	characters other than letters and numbers to be counted as the first letter of a word
	 * @return the word starting at the specified index, or the empty string if none
	 */
	public static String getWord(String line,int start,String extra,String extraFirst) {
		int end = start;
		if (end<line.length()) {
			char c = line.charAt(end);
			char c2 = (char)(c|0x20);
			if ((c2<'a' || c2>'z') && (c<'0' || c>'9') && extraFirst.indexOf(c)<0) {
				return "";	// First character isn't a word character, so no word
			}
			++end;
		}
		while (end<line.length()) {
			char c = line.charAt(end);
			char c2 = (char)(c|0x20);
			if ((c2<'a' || c2>'z') && (c<'0' || c>'9') && extra.indexOf(c)<0) {
				break;
			}
			++end;
		}
		try {
			return line.substring(start,end);
		}
		catch (StringIndexOutOfBoundsException e) {
			return "";
		}
	}

	/**
	 * Checks if <code>c</code> is a word character or not.
	 * @param c		the character to be checked
	 * @param extra	characters other than letters and numbers to be counted as part of a word
	 * @return true if <code>c</code> is a letter or number or if it is contained in <code>extra</code>
	 */
	public static boolean isWordChar(char c, String extra) {
		char c2 = (char)(c|0x20);
		return ((c2>='a' && c2<='z') || (c>='0' && c<='9') || extra.indexOf(c)>=0);
	}

	/**
	 * Checks if <code>c</code> is a word character or not.
	 * @param c		the character to be checked
	 * @param extra	characters other than letters and numbers to be counted as part of a word
	 * @return true if <code>c</code> is a letter or number or if it is contained in <code>extra</code>
	 */
	public static boolean isWordChar(char c, char[] extra) {
		char c2 = (char)(c|0x20);
		return ((c2>='a' && c2<='z') || (c>='0' && c<='9') || ArrayUtil.indexOf(extra,c)>=0);
	}

	/**
	 * Checks if all characters of <code>text</code> are word characters or not.
	 * @param text	the text to be checked
	 * @param extra	characters other than letters and numbers to be counted as part of a word
	 * @return true if <code>text</code> only contains letters or numbers or characters in <code>extra</code>
	 */
	public static boolean isAllWordChars(String text, char[] extra) {
		for (int i=0;i<text.length();++i) {
			if (!isWordChar(text.charAt(i),extra)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Calculates the length of the string if it had spaces in place of tabs (assumes tab length of 4)
	 * @param line the string to examine
	 * @return the length of the string if it had spaces in place of tabs
	 */
	public static int getUntabbedLength(String line) {
		return getUntabbedLength(line,0);
	}

	/**
	 * Calculates the length of the string if it had spaces in place of tabs, starting at a column other than 0
	 * (assumes tab length of 4)
	 * @param line the string to examine
	 * @param column the starting column of the string
	 * @return the length of the string if it had spaces in place of tabs
	 */
	public static int getUntabbedLength(String line,int column) {
		int length = 0;
		for (int i=0;i<line.length();++i) {
			if (line.charAt(i)!='\t') {
				++length;
			}
			else {
				length += 4-((length+column)&3);
			}
		}
		return length;
	}

	/**
	 * @param c the character to check
	 * @return true iff c is a valid hexadecimal character
	 */
	public static boolean isHex(char c) {
		return (c>='0' && c<='9') || ((c|0x20)>='a' && (c|0x20)<='f');
	}

	/**
	 * @param c the character to evaluate
	 * @return the hexadecimal value represented by the specified character
	 */
	public static byte fromHex(char c) {
		return (byte)((c<='9') ? c-'0' : (c|0x20)-'a'+10);
	}

	/**
	 * Reduces a string to a single word by removing all but word characters and the extra characters, and ending if any whitespace (other than space) is encountered.
	 * @param text the String to reduce to a single word
	 * @param extra the extra characters to be considered part of a word (alphanumeric characters are already considered part of a word)
	 * @return either a new String containing only the allowed characters, or the given String if it contained only allowed characters
	 */
	public static String reduceToWord(String text, char[] extra) {
		int length = 0;
		for (int i=0;i<text.length() && text.charAt(i)>=' ';++i) {
			if (isWordChar(text.charAt(i),extra)) {
				++length;
			}
		}
		if (length==text.length()) {
			return text;
		}
		char[] newText = new char[length];
		length = 0;
		for (int i=0;i<text.length() && text.charAt(i)>=' ';++i) {
			if (isWordChar(text.charAt(i),extra)) {
				newText[length++] = text.charAt(i);
			}
		}
		return new String(newText);
	}
	/**
	 * Reduces a string to a single word by removing all but word characters and the extra characters, and ending if any whitespace (other than space) is encountered.
	 * @param text the String to reduce to a single word
	 * @param extra the extra characters to be considered part of a word (alphanumeric characters are already considered part of a word)
	 * @return either a new char[] containing only the allowed characters, or the given char[] if it contained only allowed characters
	 */
	public static char[] reduceToWord(char[] text, char[] extra) {
		int length = 0;
		for (int i=0;i<text.length && text[i]>=' ';++i) {
			if (isWordChar(text[i],extra)) {
				++length;
			}
		}
		if (length==text.length) {
			return text;
		}
		char[] newText = new char[length];
		length = 0;
		for (int i=0;i<text.length && text[i]>=' ';++i) {
			if (isWordChar(text[i],extra)) {
				newText[length++] = text[i];
			}
		}
		return newText;
	}

	public static char[] reduceToNumber(char[] text, int radix) {
		int length = 0;
		boolean hasLowerCase = false;
		for (int i=0;i<text.length && text[i]>=' ';++i) {
			if (length==0 && text[i]=='-') {
				++length;
			}
			if ((text[i]>='0' && text[i]<='9' && text[i]<=('0'+radix)) || ((text[i]|0x20)>='a' && (text[i]|0x20)<='z' && (text[i]|0x20)<=('a'+radix-10))) {
				++length;
				hasLowerCase |= text[i]>='a';
			}
		}
		if (!hasLowerCase && length==text.length) {
			return text;
		}
		char[] newText = new char[length];
		length = 0;
		for (int i=0;i<text.length && text[i]>=' ';++i) {
			if (length==0 && text[i]=='-') {
				newText[length++] = text[i];
			}
			if ((text[i]>='0' && text[i]<='9' && text[i]<=('0'+radix)) || ((text[i]|0x20)>='a' && (text[i]|0x20)<='z' && (text[i]|0x20)<=('a'+radix-10))) {
				newText[length] = text[i];
				// Make all characters uppercase
				if (newText[length]>='A') {
					newText[length] &= ~0x20;
				}
				length++;
			}
		}
		return newText;
	}
}
